package org.jflame.context.filemanager;

import org.jflame.commons.codec.Transcoder;
import org.jflame.commons.file.FileHelper;
import org.jflame.commons.model.Chars;
import org.jflame.commons.util.DateHelper;
import org.jflame.commons.util.IOHelper;
import org.jflame.commons.util.MapHelper;
import org.jflame.commons.util.StringHelper;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.time.DateUtils;

import com.aliyun.oss.ClientConfiguration;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.HttpMethod;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSErrorCode;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.common.auth.DefaultCredentialProvider;
import com.aliyun.oss.common.comm.Protocol;
import com.aliyun.oss.internal.OSSHeaders;
import com.aliyun.oss.internal.OSSUtils;
import com.aliyun.oss.model.BucketReferer;
import com.aliyun.oss.model.CopyObjectRequest;
import com.aliyun.oss.model.CopyObjectResult;
import com.aliyun.oss.model.CreateBucketRequest;
import com.aliyun.oss.model.DeleteObjectsRequest;
import com.aliyun.oss.model.DeleteObjectsResult;
import com.aliyun.oss.model.DownloadFileRequest;
import com.aliyun.oss.model.DownloadFileResult;
import com.aliyun.oss.model.GeneratePresignedUrlRequest;
import com.aliyun.oss.model.ListObjectsRequest;
import com.aliyun.oss.model.OSSObject;
import com.aliyun.oss.model.OSSObjectSummary;
import com.aliyun.oss.model.ObjectListing;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PolicyConditions;
import com.aliyun.oss.model.SetBucketCORSRequest;
import com.aliyun.oss.model.SetBucketCORSRequest.CORSRule;
import com.aliyun.oss.model.SetBucketRefererRequest;

/**
 * Ali OSS云存储文件管理
 * 
 * @author charles.zhang
 */
public class AliOssFileManager extends OssFileManager<OssManagerCfg> {

    private final static String ossRootDomain = "aliyuncs.com";
    private OSSClient ossClient = null;
    // private String bucketEndpoint;// 当前bucket的访问地址

    public AliOssFileManager(OssManagerCfg cfg) {
        super(cfg);
        init();
    }

    public AliOssFileManager(String endpoint, String accessId, String accessSecret, String bucketName) {
        this(new OssManagerCfg(endpoint, accessId, accessSecret, bucketName));
    }

    private void init() {
        ClientConfiguration aliOssClientConf = new ClientConfiguration();
        aliOssClientConf.setSupportCname(true);// 开启支持CNAME选项

        OssManagerCfg managerCfg = getConfig();
        if (managerCfg.isEnableHttps()) {
            aliOssClientConf.setProtocol(Protocol.HTTPS);
        } else {
            aliOssClientConf.setProtocol(Protocol.HTTP);
        }
        // set http proxy
        if (StringHelper.isNotEmpty(managerCfg.getProxyIp())) {
            aliOssClientConf.setProxyHost(managerCfg.getProxyIp());
        }
        if (managerCfg.getProxyPort() > 0) {
            aliOssClientConf.setProxyPort(managerCfg.getProxyPort());
        }
        if (StringHelper.isNotEmpty(managerCfg.getProxyUser())) {
            aliOssClientConf.setProxyUsername(managerCfg.getProxyUser());
        }
        if (StringHelper.isNotEmpty(managerCfg.getProxyPassword())) {
            aliOssClientConf.setProxyPassword(managerCfg.getProxyPassword());
        }
        String endpoint;
        // 没有配置endpoint,根据region组装地址
        if (StringHelper.isEmpty(managerCfg.getServer())) {
            if (StringHelper.isEmpty(managerCfg.getRegion())) {
                throw new IllegalArgumentException("alioss must configure region if no server is configured");
            }
            // <region>.aliyuncs.com
            endpoint = String.format("%s.%s", managerCfg.getRegion(), ossRootDomain);
            managerCfg.setServer(endpoint);
        } else {
            // 访问地址使用自定义域名
            if (!StringHelper.containsAny(managerCfg.getServer(), aliOssClientConf.getCnameExcludeList())) {
                StringHelper.requireNonEmpty(managerCfg.getRegion(), "alioss must configure region if use cname");
                managerCfg.setCname(true);
                endpoint = String.format("%s.%s", managerCfg.getRegion(), ossRootDomain);
            } else {
                // 视为官方域名
                endpoint = managerCfg.getServer();
            }
        }

        if (managerCfg.getConnectionTimeout() > 0) {
            aliOssClientConf.setConnectionTimeout(managerCfg.connectionTimeoutInMillis());
        }
        if (managerCfg.getReadTimeout() > 0) {
            aliOssClientConf.setSocketTimeout(managerCfg.readTimeoutInMillis());
        }
        ossClient = (OSSClient) OSSClientBuilder.create()
                .endpoint(endpoint)
                .region(managerCfg.getRegion())
                .clientConfiguration(aliOssClientConf)
                .credentialsProvider(
                        new DefaultCredentialProvider(managerCfg.getAccessId(), managerCfg.getAccessSecret()))
                .build();
        // ossClient = new OSSClient(endpoint, managerCfg.getAccessId(), managerCfg.getAccessSecret(),
        // aliOssClientConf);
        if (!managerCfg.isCname()) {

            String bucketEndpoint = OSSUtils
                    .determineFinalEndpoint(ossClient.getEndpoint(), managerCfg.getBucket(),
                            ossClient.getClientConfiguration())
                    .toString();
            managerCfg.setServer(bucketEndpoint);
        }
    }

    @Override
    public String save(File file, String saveDir, Map<String,String> fileMeta) throws FileAccessException {
        String fileId = createNewFileId(saveDir, file.getName());
        ObjectMetadata meta = new ObjectMetadata();
        meta.setUserMetadata(fileMeta);

        try {
            ossClient.putObject(getCurrentBucket(), fileId, file, meta);
        } catch (ClientException | OSSException e) {
            throw new FileAccessException("上传文件失败,文件:" + file.getName(), e);
        }
        return fileId;
    }

    @Override
    public String save(InputStream fileStream, String filePath, Map<String,String> fileMeta, boolean replaceExisting)
            throws FileAccessException {
        String fileId;
        if (replaceExisting) {
            fileId = filePath;
        } else {
            fileId = createNewFileId(StringHelper.substringBeforeLast(filePath, "/"), FileHelper.getFilename(filePath));
        }
        ObjectMetadata meta = new ObjectMetadata();
        meta.setUserMetadata(fileMeta);
        if (meta.getContentLength() == 0) {
            if (fileStream instanceof ByteArrayInputStream || fileStream instanceof FileInputStream
                    || fileStream instanceof BufferedInputStream) {
                int objsize;
                try {
                    objsize = fileStream.available();
                    if (objsize > 0) {
                        meta.setContentLength(objsize);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        try {
            ossClient.putObject(getCurrentBucket(), fileId, fileStream, meta);
        } catch (ClientException | OSSException e) {
            throw new FileAccessException("上传文件失败", e);
        } finally {
            IOHelper.closeQuietly(fileStream);
        }
        return fileId;
    }

    @Override
    public byte[] readBytes(String filePath) throws FileAccessException {
        InputStream contentStream = null;
        String bucket = getCurrentBucket();
        String key = getFileId(filePath);
        try (OSSObject ossObject = ossClient.getObject(bucket, key)) {
            contentStream = ossObject.getObjectContent();
            if (contentStream != null) {
                return IOHelper.readBytes(contentStream);
            }
            throw new FileAccessException(key + "不存在于alioss bucket:" + bucket, FileAccessException.FILE_NOT_FOUND);
        } catch (OSSException e) {
            if (e.getErrorCode() == OSSErrorCode.NO_SUCH_KEY) {
                throw new FileAccessException("读取文件异常:" + filePath, FileAccessException.FILE_NOT_FOUND, e);
            } else {
                throw new FileAccessException("读取文件异常:" + filePath, e);
            }
        } catch (IOException | ClientException e) {
            throw new FileAccessException("读取文件异常:" + filePath, e);
        } finally {
            IOHelper.closeQuietly(contentStream);
        }
    }

    @Override
    public File read(String filePath) throws FileAccessException {
        InputStream contentStream = null;
        String key = getFileId(filePath);
        Path tmpPath = mappingTmpFile(key);
        // 使用缓存的临时文件
        Optional<File> localCacheFileOpl = getTmpCacheFile(tmpPath);
        if (localCacheFileOpl.isPresent()) {
            return localCacheFileOpl.get();
        }
        String bucket = getCurrentBucket();
        try (OSSObject ossObject = ossClient.getObject(bucket, key);) {
            contentStream = ossObject.getObjectContent();
            if (contentStream != null) {
                Files.copy(contentStream, tmpPath, StandardCopyOption.REPLACE_EXISTING);
                File tmpFile = tmpPath.toFile();
                return tmpFile;
            }
            logger.warn("alioss文件不存在,filePath:{}, key:{}, bucket:{}", filePath, key, bucket);
            throw new FileAccessException(key + " 不存在", FileAccessException.FILE_NOT_FOUND);
        } catch (OSSException e) {
            if (e.getErrorCode() == OSSErrorCode.NO_SUCH_KEY) {
                throw new FileAccessException("读取文件异常:" + filePath, FileAccessException.FILE_NOT_FOUND, e);
            } else {
                throw new FileAccessException("读取文件异常:" + filePath, e);
            }
        } catch (IOException | ClientException e) {
            throw new FileAccessException("读取文件异常:" + filePath, e);
        }
    }

    @Override
    public InputStream readStream(String filePath) throws FileAccessException {
        String key = getFileId(filePath);
        String bucket = getCurrentBucket();
        try {
            OSSObject ossObject = ossClient.getObject(bucket, key);
            return ossObject.getObjectContent();
        } catch (OSSException e) {
            if (e.getErrorCode() == OSSErrorCode.NO_SUCH_KEY) {
                throw new FileAccessException("读取文件异常:" + filePath, FileAccessException.FILE_NOT_FOUND, e);
            } else {
                throw new FileAccessException("读取文件异常:" + filePath, e);
            }
        } catch (ClientException e) {
            throw new FileAccessException("读取文件异常:" + filePath, e);
        }
    }

    @Override
    public void download(String filePath, Path savePath) throws FileAccessException {
        String key = getFileId(filePath);
        String bucket = getCurrentBucket();
        DownloadFileRequest downloadRequest = new DownloadFileRequest(bucket, key);
        downloadRequest.setDownloadFile(savePath.toString());
        try {
            DownloadFileResult result = ossClient.downloadFile(downloadRequest);
            if (logger.isDebugEnabled()) {
                logger.debug("alioss文件下载,key:{},localFile:{},result:{}", filePath, savePath, result);
            }
        } catch (OSSException e) {
            if (e.getErrorCode() == OSSErrorCode.NO_SUCH_KEY) {
                throw new FileAccessException("下载文件异常:" + filePath, FileAccessException.FILE_NOT_FOUND, e);
            } else {
                throw new FileAccessException("下载文件异常:" + filePath, e);
            }
        } catch (Throwable e) {
            throw new FileAccessException("下载文件异常:" + filePath, e);
        }
    }

    /**
     * 删除文件,多个文件必须是同一bucket的
     */
    @Override
    public int delete(String... filePaths) throws FileAccessException {
        return delete(getCurrentBucket(), filePaths);
    }

    @Override
    public int delete(String bucketName, String[] filePaths) throws FileAccessException {
        if (ArrayUtils.isEmpty(filePaths)) {
            return 0;
        }
        List<String> fileKeys = Arrays.asList(filePaths);
        if (logger.isInfoEnabled()) {
            logger.info("删除文件:{}", ArrayUtils.toString(fileKeys));
        }

        for (int i = 0; i < filePaths.length; i++) {
            fileKeys.set(i, getFileId(fileKeys.get(i)));
        }

        try {
            DeleteObjectsRequest deleteRequest = new DeleteObjectsRequest(
                    StringHelper.isEmpty(bucketName) ? getCurrentBucket() : bucketName).withKeys(fileKeys);
            deleteRequest.setQuiet(true);// 只返回出错的结果
            DeleteObjectsResult deleteObjectsResult = ossClient.deleteObjects(deleteRequest);
            List<String> deletedObjects = deleteObjectsResult.getDeletedObjects();
            return deletedObjects.size();
        } catch (OSSException | ClientException e) {
            throw new FileAccessException("删除文件异常", e);
        }
    }

    @SuppressWarnings("unused")
    @Override
    public String copy(final String srcFilePath, String destFilePath) throws FileAccessException {
        if (Objects.equals(srcFilePath, destFilePath)) {
            // ("源文件与目标文件路径相同");
            logger.warn("alioss复制文件, 源文件与目标文件相同,源:{}", srcFilePath);
            return destFilePath;
        }

        String srcBucket = getCurrentBucket();
        String srckey = getFileId(srcFilePath);

        String destFile;
        String destBucket;
        CopyObjectRequest copyObjectRequest = null;

        if (StringHelper.isEmpty(destFilePath)) {
            int extBefore = srckey.lastIndexOf('.');
            if (extBefore < 1) {
                extBefore = srckey.length();
            }

            destFile = StringHelper.insertAt(srckey, String.format(copy_name_fmt, DateHelper.formatNow("yyMMddHHmm"),
                    RandomStringUtils.random(3, true, true)), extBefore);
            destBucket = srcBucket;

        } else {
            // 复制到别的bucket,目标路径以bucket:开头
            if (destFilePath.startsWith("bucket:")) {
                destBucket = StringHelper.substringBetween(destFilePath, "bucket:", "/");
                destFilePath = StringHelper.substringAfter(destFilePath, "/");
            } else {
                destBucket = getCurrentBucket();
            }
            // 复制到目录，文件名不变
            if (StringHelper.endChar(destFilePath) == Chars.SLASH) {
                destFile = destFilePath + FileHelper.getFilename(srckey);
            } else {
                destFile = destFilePath;
            }

        }
        copyObjectRequest = new CopyObjectRequest(srcBucket, srckey, destBucket, destFile);
        copyObjectRequest.addHeader("x-oss-forbid-overwrite", "true");
        try {
            CopyObjectResult copyResult = ossClient.copyObject(copyObjectRequest);
            // System.out.println(copyResult.getETag());
        } catch (OSSException | ClientException e) {
            logger.error("alioss复制文件异常,参数: 源={},目标=", srcFilePath, destFilePath);
            throw new FileAccessException("复制文件异常", e);
        }
        return destFile;
    }

    public Map<String,String> generatePostObjectParams(PresignedUrlArgs args) {
        Map<String,String> respMap = new LinkedHashMap<String,String>();
        String fileId = createNewFileId(args.getSavePath(), args.getFileNameOrExt());

        Optional<String> mimeType = guessContentType(fileId);
        // 上传限定条件
        PolicyConditions policyConds = new PolicyConditions();
        if (args.getMaxFileSize() > 0) {
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, args.getMaxFileSize());
        }
        policyConds.addConditionItem(PolicyConditions.COND_KEY, fileId);
        if (mimeType.isPresent()) {
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_TYPE, mimeType.get());
            respMap.put(PolicyConditions.COND_CONTENT_TYPE, mimeType.get());
        }

        Date expiration = DateUtils.addSeconds(new Date(), args.getExpireTime());

        String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
        String encodedPolicy = Transcoder.encodeBase64String(postPolicy);
        String postPolicySignature = ossClient.calculatePostSignature(postPolicy);

        respMap.put("OSSAccessKeyId", getConfig().getAccessId());
        respMap.put("policy", encodedPolicy);
        respMap.put("Signature", postPolicySignature);
        respMap.put("key", fileId);
        respMap.put("host", ossClient.getEndpoint()
                .toString());

        return respMap;
    }

    @Override
    public SignedUrl generatePresignedPutUrl(PresignedUrlArgs args) {
        return generatePresignedUrl(HttpMethod.PUT, args);
    }

    @Override
    public String generatePresignedGetUrl(PresignedUrlArgs args) {
        return generatePresignedUrl(HttpMethod.GET, args).urlStr();
    }

    private SignedUrl generatePresignedUrl(HttpMethod method, PresignedUrlArgs args) {
        // https://help.aliyun.com/zh/oss/add-signatures-to-urls?spm=a2c4g.11186623.0.i10#concept-xqh-2df-xdb
        // 返回给终端的元数据
        Map<String,String> respMap = new HashMap<>();
        GeneratePresignedUrlRequest request = null;
        if (method == HttpMethod.GET) {
            String fileId = getFileId(args.getFileNameOrExt());
            request = new GeneratePresignedUrlRequest(getCurrentBucket(), fileId, HttpMethod.GET);

        } else if (method == HttpMethod.PUT) {
            String newName = createNewFileId(args.getSavePath(), args.getFileNameOrExt());
            request = new GeneratePresignedUrlRequest(getCurrentBucket(), newName, HttpMethod.PUT);

            Map<String,String> headers = setContentType(newName, args.getHeaders());
            // OSSHeaders
            if (MapHelper.isNotEmpty(headers)) {
                request.setHeaders(headers);
                respMap.putAll(headers);
            }

            // 添加用户自定义元信息。
            if (MapHelper.isNotEmpty(args.getUserMetadata())) {
                request.setUserMetadata(args.getUserMetadata());
                args.getUserMetadata()
                        .forEach((k, v) -> respMap.put(OSSHeaders.OSS_USER_METADATA_PREFIX + k, v));
            }
        } else {
            throw new IllegalArgumentException("alioss预签名url只支持get或put方式");
        }
        if (args.getQueryParams() != null) {
            request.setQueryParameter(args.getQueryParams());
        }
        Date expiration = DateUtils.addSeconds(new Date(), args.getExpireTime());
        request.setExpiration(expiration);

        try {
            URL surl = ossClient.generatePresignedUrl(request);
            return new SignedUrl(surl, respMap);
        } catch (OSSException | ClientException e) {
            throw new RuntimeException("生成alioss签名上传url异常", e);
        }
    }

    @Override
    public List<String> listFiles(String dir, int maxFiles, boolean isExcludeDir) throws FileAccessException {
        ListObjectsRequest listObjectsRequest = new ListObjectsRequest(getCurrentBucket());
        if (maxFiles <= 0) {
            maxFiles = 1000;// 最大数
        }
        listObjectsRequest.setMaxKeys(maxFiles);
        int dirLen = dir != null ? dir.length() : 0;
        if (dirLen > 0 && !"/".equals(dir)) {
            if (StringHelper.endChar(dir) != Chars.SLASH) {
                dir = dir + Chars.SLASH;
                dirLen += 1;
            }
            listObjectsRequest.setPrefix(dir);
        }
        List<String> filenames = new ArrayList<>(maxFiles);
        ObjectListing listing;
        try {
            listing = ossClient.listObjects(listObjectsRequest);
        } catch (OSSException | ClientException e) {
            throw new FileAccessException("列举目录文件异常:" + dir, e);
        }
        for (OSSObjectSummary objSummary : listing.getObjectSummaries()) {
            filterFilename(objSummary.getKey(), dirLen, isExcludeDir, filenames);
        }
        return filenames;
    }

    @Override
    public void createBucket(String bucketName) {
        CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
        ossClient.createBucket(createBucketRequest);
    }

    /**
     * 设置bucket的请求防盗链http referer
     * 
     * @param allowEmptyReferer 是否允许空的http referer
     * @param refererList 允许的http referer列表
     * @param append true=追加,false=替换更新
     */
    public void setBucketReferer(boolean allowEmptyReferer, List<String> refererList, boolean append) {
        String bucketName = getCurrentBucket();
        SetBucketRefererRequest request = new SetBucketRefererRequest(bucketName);
        BucketReferer referer = null;
        if (append) {
            referer = ossClient.getBucketReferer(bucketName);
            if (refererList != null) {
                referer.getRefererList()
                        .addAll(refererList);
            }
            referer.setAllowEmptyReferer(allowEmptyReferer);
        } else {
            referer = new BucketReferer(allowEmptyReferer, refererList);
        }
        request.setReferer(referer);
        ossClient.setBucketReferer(request);

    }

    /**
     * 设置跨域规则
     * 
     * @param allowedOrigins
     * @param allowedMethods
     * @param allowedHeaders
     * @param exposeHeaders
     * @param maxAgeSeconds
     */
    public void SetBucketCORS(List<String> allowedOrigins, List<String> allowedMethods, List<String> allowedHeaders,
            List<String> exposeHeaders, Integer maxAgeSeconds) {
        SetBucketCORSRequest request = new SetBucketCORSRequest(getCurrentBucket());
        CORSRule rule = new CORSRule();
        rule.setAllowedOrigins(allowedOrigins);
        rule.setAllowedHeaders(allowedHeaders);
        rule.setAllowedMethods(allowedMethods);
        rule.setExposeHeaders(exposeHeaders);
        if (maxAgeSeconds != null) {
            rule.setMaxAgeSeconds(maxAgeSeconds);
        }
        request.addCorsRule(rule);

        ossClient.setBucketCORS(request);
    }

    @Override
    public void close() {
        if (ossClient != null) {
            ossClient.shutdown();
        }
    }

    public OSSClient getClient() {
        return ossClient;
    }

}
